﻿using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
using System.Xml;

namespace SuppKids.Licensing.LicenseManager.Utilities.RSA
{
    public class RSACryptoTransformer
	{
		private readonly RSACryptoServiceProvider cryptoServiceProvider;

        public RSACryptoTransformer(RSACryptoServiceProvider cryptoServiceProvider)
		{
			this.cryptoServiceProvider = cryptoServiceProvider;
		}

		public virtual void Sign(XmlDocument xmlDoc)
		{
			if (xmlDoc.DocumentElement == null)
			{
				throw new Exception("xml document is empty.");
			}

			var signedXml = new SignedXml(xmlDoc) { SigningKey = this.cryptoServiceProvider };
			var reference = new Reference { Uri = string.Empty };
			reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
			signedXml.AddReference(reference);

			signedXml.ComputeSignature();

			XmlElement xmlDigitalSignature = signedXml.GetXml();

			xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(xmlDigitalSignature, true));
		}

		public virtual bool Verify(XmlDocument xmlDoc)
		{
			var signedXml = new SignedXml(xmlDoc);
			XmlNodeList nodeList = xmlDoc.GetElementsByTagName("Signature");

			if (nodeList.Count == 0)
			{
				throw new CryptographicException("Verification failed: No Signature was found in the document.");
			}

			if (nodeList.Count > 1)
			{
				throw new CryptographicException("Verification failed: More that one signature was found for the document.");
			}

			signedXml.LoadXml((XmlElement)nodeList[0]);

			return signedXml.CheckSignature(this.cryptoServiceProvider);
		}

		public Stream Encrypt(Stream input)
		{
			const int bufferSize = 32;
			return ReadAndApply(input, bufferSize, x => this.cryptoServiceProvider.Encrypt(x, false));
		}

		public Stream Encrypt(XmlDocument xmlDocument)
		{
			using (var input = new MemoryStream())
			{
				xmlDocument.Save(input);
				return this.Encrypt(input);
			}
		}

		public virtual Stream Decrypt(Stream input)
		{
			// RSA key size 2048, for RSA key size 1024 bufferSize = 128
			const int bufferSize = 256;
			return ReadAndApply(input, bufferSize, x => this.cryptoServiceProvider.Decrypt(x, false));
		}

		private static Stream ReadAndApply(Stream input, int bufferSize, Func<byte[], byte[]> cryptoFunction)
		{
			if (input.CanSeek)
			{
				input.Seek(0, SeekOrigin.Begin);
			}

			var resultStream = new MemoryStream();

			var buffer = new byte[bufferSize];
			int read;
			while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
			{
				var readBytes = new byte[read];
				Array.Copy(buffer, readBytes, read);
				var bytes = cryptoFunction(readBytes);
				resultStream.Write(bytes, 0, bytes.Length);
			}

			resultStream.Seek(0, SeekOrigin.Begin);

			return resultStream;
		}
	}
}
